package de.dailab.graphworld.bean;

import de.dailab.graphworld.bean.memory.AgentInfo;
import de.dailab.graphworld.ontology.AgentType;
import de.dailab.graphworld.ontology.BrokeringProtocol;
import de.dailab.graphworld.ontology.Edge;
import de.dailab.graphworld.ontology.Node;
import de.dailab.graphworld.ontology.PositioningMessage;
import de.dailab.graphworld.ontology.RequestProtocol;
import de.dailab.graphworld.ontology.Registration;
import de.dailab.graphworld.ontology.RegistrationRequest;
import de.dailab.graphworld.ontology.Reply;
import de.dailab.graphworld.ontology.ReplyType;
import de.dailab.graphworld.ontology.Task;
import de.dailab.graphworld.pathfinding.AStar;
import de.dailab.jiactng.agentcore.comm.ICommunicationAddress;
import de.dailab.jiactng.agentcore.comm.IGroupAddress;
import de.dailab.jiactng.agentcore.comm.IMessageBoxAddress;
import de.dailab.jiactng.agentcore.comm.message.JiacMessage;
import de.dailab.jiactng.agentcore.knowledge.IFact;
import de.dailab.jiactng.agentcore.ontology.IAgentDescription;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 *
 * @author Kseniya
 */
public class BeanBroker extends BeanTeamMember {

    private final List<ICommunicationAddress> refusedAssignees = new ArrayList<>();
    private final Map<ICommunicationAddress, Node> positions = new HashMap<>();

    @Override
    public void doInit() throws Exception {
        // store known agents here
        List<IAgentDescription> emptyList = Collections.emptyList();
        memory.write(new AgentInfo(AgentType.SCOUT, emptyList));
        memory.write(new AgentInfo(AgentType.CARRIER, emptyList));
    }

    @Override
    public void execute() {
        super.execute();
        // call for registration in the first execution
        AgentInfo scoutInfo = memory.read(new AgentInfo(AgentType.SCOUT, null));
        if (scoutInfo.getAgents().isEmpty()) {
            callForRegistration(AgentType.SCOUT);
        }
        AgentInfo carrierInfo = memory.read(new AgentInfo(AgentType.CARRIER, null));
        if (carrierInfo.getAgents().isEmpty()) {
            callForRegistration(AgentType.CARRIER);
        }
    }

    private void callForRegistration(AgentType type) {
        sendMsgGroup(new RegistrationRequest(), type.getAddress());
    }

    private void consumeRegistrationReply(Registration msg) {
        AgentInfo agentInfoTemplate = new AgentInfo(msg.getType(), null);
        AgentInfo storedAgentInfo = memory.read(agentInfoTemplate);
        ArrayList<IAgentDescription> agentsUpdated = new ArrayList<>(storedAgentInfo.getAgents().size());
        // discard all previous remembrances of replied agent
        for (IAgentDescription iAgentDesc : storedAgentInfo.getAgents()) {
            if (!iAgentDesc.getAid().equals(msg.getAgent().getAid())) {
                agentsUpdated.add(iAgentDesc);
            }
        }
        agentsUpdated.add(msg.getAgent());
        agentsUpdated.trimToSize();
        memory.update(agentInfoTemplate, new AgentInfo(msg.getType(), agentsUpdated));
    }

    @Override
    protected Collection<IGroupAddress> getGroups() {
        // broker needs to communicate both with scouts and carriers
        return Arrays.asList(new IGroupAddress[]{AgentType.SCOUT.getAddress(), AgentType.CARRIER.getAddress()});
    }

    @Override
    public void consume(JiacMessage msg) {
        IFact payload = memory.remove(msg).getPayload();
        // collect registration replies
        if (payload instanceof Registration) {
            Registration reply = (Registration) payload;
            // remember the replied agent
            consumeRegistrationReply(reply);
        } else if (payload instanceof PositioningMessage) {
            PositioningMessage positionMsg = (PositioningMessage) payload;
            positions.put(msg.getSender(), positionMsg.getPosition());
        } // a task from scout, delegate it to carrier(s)
        else if (payload instanceof Task) {
            Task task = (Task) payload;
            AgentInfo carriersInfo = memory.read(new AgentInfo(AgentType.CARRIER, null));
            // handle no-match-failure
            if (carriersInfo.getAgents().isEmpty()) {
                sendMsgPersonal(new Reply(task, BrokeringProtocol.FAILURE_NO_MATCH), msg.getSender());
            } else if (!refusedAssignees.isEmpty()) {
                // the broker is still occupied with the previous task
                sendMsgPersonal(new Reply(task, BrokeringProtocol.REFUSE), msg.getSender());
            } else {
                sendMsgPersonal(new Reply(task, BrokeringProtocol.AGREE), msg.getSender());
                initiateJob(task);
            }
        } else if (payload instanceof Reply) {
            Reply reply = (Reply) payload;
            updateJobState(msg.getSender(), reply);
        } else if (payload instanceof PositioningMessage) {
            Node agentPosition = ((PositioningMessage) payload).getPosition();
            positions.put(msg.getSender(), agentPosition);
        }
    }

    private ICommunicationAddress selectAgent(final Task task) {
        // select best agent for the task by shortest distance between agent's position and gold node
        List<IAgentDescription> agents = new ArrayList<>(memory.read(new AgentInfo(AgentType.CARRIER, null)).getAgents());
        Collections.sort(agents, new Comparator<IAgentDescription>() {
            @Override
            public int compare(IAgentDescription thisAgent, IAgentDescription anotherAgent) {
                Node thisPos = positions.get(thisAgent.getMessageBoxAddress());
                if (thisPos == null || task.goldNode == null) {
                    System.out.println("ERROR!!! Stored position or task are null");
                    return 0;
                }
                List<Edge> thisPath;
                if (thisPos.getID() == task.goldNode.getID()) {
                    thisPath = Collections.emptyList();
                } else {
                    thisPath = AStar.findPath(thisPos, task.goldNode);
                }
                Node anotherPos = positions.get(anotherAgent.getMessageBoxAddress());
                if (anotherPos == null || task.goldNode == null) {
                    System.out.println("ERROR!!! Stored position or task are null");
                    return 0;
                }
                List<Edge> anotherPath;

                if (anotherPos.getID() == task.goldNode.getID()) {
                    anotherPath = Collections.emptyList();
                } else {
                    anotherPath = AStar.findPath(anotherPos, task.goldNode);
                }
                return new Integer(thisPath.size()).compareTo(anotherPath.size());
            }
        });
        // select first that did not refuse
        for (IAgentDescription agent : agents) {
            IMessageBoxAddress agentAddress = agent.getMessageBoxAddress();
            if (!refusedAssignees.contains(agentAddress)) {
                return agentAddress;
            }
        }
        return null;
    }

    private void initiateJob(Task task) {
        ICommunicationAddress selectedAgent = selectAgent(task);
        // invite to take part on the job
        sendMsgPersonal(task, selectedAgent);
    }

    private void updateJobState(ICommunicationAddress assignee, Reply reply) {
        ReplyType type = reply.getType();
        if (RequestProtocol.AGREE.equals(type)) {
//            System.out.println("broker found assignee for task " + reply.getTask());
            // agent assigned, current job done
            refusedAssignees.clear();
            return;
        }
        ICommunicationAddress scoutAddress = memory.read(new AgentInfo(AgentType.SCOUT, null)).getAgents().iterator().next().getMessageBoxAddress();
        if (RequestProtocol.FAILURE.equals(type)) {
            // inform scout that the task has failed
            sendMsgPersonal(new Reply(reply.getTask(), BrokeringProtocol.SUBPROTOCOL_FAILURE), scoutAddress);
        } else if (RequestProtocol.INFORM_RESULT.equals(type)) {
            // inform scout that the task is finished
            sendMsgPersonal(new Reply(reply.getTask(), BrokeringProtocol.SUBPROTOCOL_PROCEED), scoutAddress);
        } else if (RequestProtocol.REFUSE.equals(type)) {
            refusedAssignees.add(assignee);
            AgentInfo carriersInfo = memory.read(new AgentInfo(AgentType.CARRIER, null));
            boolean allAgentsRefused = refusedAssignees.size() == carriersInfo.getAgents().size();
            // if all agents refused to do the task, cancel the job and inform the scout
            if (allAgentsRefused) {
//                System.out.println("all agents refused on task " + reply.getTask());
                sendMsgPersonal(new Reply(reply.getTask(), BrokeringProtocol.FAILURE_NO_MATCH), scoutAddress);
                refusedAssignees.clear();
            } else {
                // select another agent for the job
                ICommunicationAddress selectedAgent = selectAgent(reply.getTask());
                sendMsgPersonal(reply.getTask(), selectedAgent);
            }
        }
    }
}
